/**
 * 程序功能：使用MQTT协议，与阿里云物联网平台进行数据通信。
 *          在本示例程序中，实现了NTP时钟同步，即获取网络时间。
 * 使用教程：https://blog.yyzt.site/
 * 注意事项：1.所连接的WIFI名称和WIFI需要设置正确，并且仅支持连接2.4GHz的WIFI，不支持5GHz的WIFI
 *          2.阿里云物联网平台的设备证书（也叫三元组，ProductKey、DeviceName、DeviceSecret）需要设置正确
 *          3.相关属性名称标识符和数据类型需要设置正确
 * 参考资料：https://github.com/0xYootou/arduino-aliyun-iot-sdk
 *          https://github.com/legenddcr/aliyun-mqtt-arduino
 *          https://help.aliyun.com/document_detail/89301.html
 * 阿里云物联网平台网址：https://iot.console.aliyun.com/lk/summary/new
 * NTP服务文档：https://help.aliyun.com/document_detail/102509.html
 */

#include <Arduino.h>          // 导入Arduino库文件
#include <YYZT_AliyunMqtt.h>  // 导入YYZT_AliyunMqtt库文件

/**
 * 串口调试打印相关参数
 */
#define debugState true             // 串口调试打印状态，true--开启调试打印，false--关闭调试打印
#define debugSerial Serial          // 调试打印的串口
#define debugSerialBuadrate 115200  // 调试打印的串口波特率

/**
 * 阿里云物联网平台相关参数
 */
#define productKey "a17RF1Mnqss"                         // 产品密钥 ProductKey
#define deviceName "ESP8266_001"                         // 设备名称 DeviceName
#define deviceSecret "7bb4991a4beabf6a14918a76819cd3bc"  // 设备密钥 DeviceSecret

String ntpRequestTopic = "/ext/ntp/" + String(productKey) + "/" + String(deviceName) + "/request";    // NTP时钟同步请求Topic
String ntpResponseTopic = "/ext/ntp/" + String(productKey) + "/" + String(deviceName) + "/response";  // NTP时钟同步响应Topic

/**
 * WIFI相关参数
 */
#define wifiName "YYZT_2.4G"       // WIFI名称
#define wifiPassword "1234567890"  // WIFI密码

/**
 * MQTT相关参数
 */
#define mqttHeartbeatTime 60        // 心跳间隔时间，默认为60s，单位为：秒
#define mqttPacketSize 1024         // MQTT数据包的大小设置最大为1024字节
#define ckeckMqttConnectTime 10000  // 检查MQTT连接的间隔时间，默认为10秒
AliyunMqtt aliyunMqtt;              // 实例化aliyunMqtt对象

// MQTT的连接状态
typedef enum {
  MqttDisconnected = 0,  // MQTT已断开连接
  MqttConnecting,        // MQTT连接中
  MqttConnected          // MQTT已连接
} MqttConnectState;
uint8_t nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态，初始化为MqttDisconnected

/*
 * 时间相关参数
 */
bool setTimeSuccessState = false;  // 设置时间成功的状态

/**
 * 函数功能：NTP时钟同步请求，连接MQTT服务器成功后只获取1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void ntpRequest() {
  StaticJsonDocument<64> jsonData;                                     // 实例化一个为64字节容量的StaticJsonDocument对象
  jsonData["deviceSendTime"] = String(millis());                       // 设备当前时间戳
  String data = "";                                                    // 定义一个存放json对象转化成字符串的变量
  serializeJson(jsonData, data);                                       // 将JSON对象转换成字符串，即反序列化
  uint16_t result = aliyunMqtt.sendCustomData(ntpRequestTopic, data);  // 发送数据到云平台
#if debugState
  debugSerial.println("NTP时钟同步请求，发送数据为：[" + String(data) + "]，发送结果为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
}

/**
 * 函数功能：NTP时钟同步响应回调
 * 参数1：[_data] [JsonVariant] JSON对象
 * 返回值：无
 * 注意事项：无
 */
void ntpResponseCallback(JsonVariant _data) {
  String deviceRecvTime = String(millis());         // 获取设备端接收消息的时间戳
  String deviceSendTime = _data["deviceSendTime"];  // 获取设备端发送消息的时间戳
  String serverRecvTime = _data["serverRecvTime"];  // 获取服务端接收消息的时间戳
  String serverSendTime = _data["serverSendTime"];  // 获取服务端发送消息的时间戳

  uint64_t nowTime = (atoll(serverRecvTime.c_str()) + atoll(serverSendTime.c_str()) + atoll(deviceRecvTime.c_str()) - atoll(deviceSendTime.c_str())) / 2;  // 计算当前精确的Unix时间

  setTimeSuccessState = true;  // 设置时间成功的状态为true

  struct timeval tv;                                  // 创建一个存放设置RTC时间的结构体
  tv.tv_sec = (time_t)(nowTime / 1000);               // 当前RTC时间的秒数值
  tv.tv_usec = (suseconds_t)(nowTime % 1000 * 1000);  // 当前RTC时间的微秒数值
  settimeofday(&tv, NULL);                            // 设置当前的RTC时间
  setenv("TZ", "CST-8", 1);                           // 设置时区为中国东八区
  tzset();                                            // 设置时区
#if debugState
  debugSerial.println("云端同步时间戳为：" + String(nowTime));
#endif
}

/**
 * 函数功能：打印时间，1秒钟更新1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void printTime() {
  static uint32_t nowTime = millis();
  if (setTimeSuccessState && millis() - nowTime >= 1000) {  // 如果设置时间成功的状态为true，并且到了打印时间
    nowTime = millis();                                     // 更新时间

    struct tm timeinfo;            // 创建一个存放时间信息的结构体
    time_t now;                    // 创建一个存放秒级时间戳的变量
    time(&now);                    // 更新当前的秒级时间戳变量
    localtime_r(&now, &timeinfo);  // 更新存放时间信息结构体

    char timeDate[50] = {0};                          // 定义一个存放当前时间的字符串数组
    sprintf(timeDate, "%d/%02d/%02d %02d:%02d:%02d",  // 拼接字符串，年、月、日、时、分、秒
            timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
            timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
#if debugState
    debugSerial.println("当前时间为：" + String(timeDate));
#endif
  }
}

/**
 * 函数功能：MQTT连接事件的回调函数
 * 参数1：[_sessionPresent] [bool] 会话是否存在
 * 返回值：无
 * 注意事项：无
 */
void onMqttConnect(bool _sessionPresent) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已连接，会话存在状态为：" + String(_sessionPresent));
#endif
  nowMqttConnectState = MqttConnected;                               // 当前MQTT的连接状态为已连接
  ntpRequest();                                                      // NTP时钟同步请求，连接MQTT服务器成功后只获取1次
  aliyunMqtt.subscribeTopic(ntpResponseTopic, ntpResponseCallback);  // 订阅NTP时钟同步响应主题和设置响应回调
}

/**
 * 函数功能：MQTT断开连接事件的回调函数
 * 参数1：[_reason] [AsyncMqttClientDisconnectReason] MQTT断开连接的原因，在AsyncMqttClientDisconnectReason枚举中查看
 * 返回值：无
 * 注意事项：无
 */
void onMqttDisconnect(AsyncMqttClientDisconnectReason _reason) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已断开连接！原因为：" + String(uint8_t(_reason)));
#endif
  nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态为已断开连接
}

/**
 * 函数功能：检查MQTT和重连
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void checkMqttAndReconnect() {
  static uint8_t mqttConnectStateMachine = 0;  // MQTT连接的状态机
  static uint32_t nowTime = 0;                 // 检查时间
  switch (mqttConnectStateMachine) {
    // 检查MQTT的连接状态
    case 0: {
      if (MqttDisconnected == nowMqttConnectState) {  // 如果当前MQTT的连接状态为已断开连接
        aliyunMqtt.mqttClient.disconnect(true);       // 断开之前的连接
        aliyunMqtt.mqttClient.connect();              // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
        mqttConnectStateMachine = 1;                  // MQTT连接中，状态机=1，重连中
        nowTime = millis();                           // 更新检查时间
#if debugState
        debugSerial.println("阿里云MQTT服务器已断开连接！开始尝试重连中......");
#endif
      }
    } break;
    // MQTT重连中
    case 1: {
      if (MqttConnected == nowMqttConnectState || millis() - nowTime > ckeckMqttConnectTime) {  // 如果当前MQTT的连接状态为已连接或者超过重连时间
        mqttConnectStateMachine = 0;                                                            // 状态机=0，回到检查中
      }
    } break;

    default:
      break;
  }
}

/**
 * 函数功能：程序初始化时调用，仅执行一次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void setup() {
  delay(1000);  // 等待设备上电稳定

#if debugState
  debugSerial.begin(debugSerialBuadrate);  // 初始化调试打印的串口
  debugSerial.println("\r\n程序开始初始化......");
#endif

  WiFi.disconnect(true);                   // 断开当前WIFI连接
  WiFi.mode(WIFI_STA);                     // 设置WIFI模式为STA模式
  WiFi.begin(wifiName, wifiPassword);      // 开始连接WIFI
  while (WL_CONNECTED != WiFi.status()) {  // 判断WIFI的连接状态
    delay(500);
#if debugState
    debugSerial.print(".");
#endif
  }

  aliyunMqtt.setDebug(debugSerial);                                       // 设置是否开启aliyunMqtt库中的打印，传入串口即开启打印
  aliyunMqtt.setDeviceCertificate(productKey, deviceName, deviceSecret);  // 设置连接阿里云物联网平台的设备证书（也叫三元组）

  aliyunMqtt.mqttClient.setKeepAlive(mqttHeartbeatTime);    // 设置心跳间隔时间
  aliyunMqtt.mqttClient.setMaxTopicLength(mqttPacketSize);  // 设置MQTT数据包大小
  aliyunMqtt.mqttClient.onConnect(onMqttConnect);           // 设置MQTT连接事件的回调函数
  aliyunMqtt.mqttClient.onDisconnect(onMqttDisconnect);     // 设置MQTT断开连接事件的回调函数
  aliyunMqtt.mqttClient.connect();                          // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
  nowMqttConnectState = MqttConnecting;                     // 当前MQTT的连接状态为连接中

#if debugState
  debugSerial.println("\r\n程序初始化完成！");
#endif
}

/**
 * 函数功能：程序主循环函数，重复运行
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void loop() {
  checkMqttAndReconnect();  // 检查MQTT和重连
  printTime();              // 打印时间，1秒钟更新1次
}
